
React Router 7
How to use the Framework mode
React Router is a multi-strategy router for React, which means it can be used in different ways depending on the needs of your application. In this guide, we’ll explore the framework mode, which is a new way to use React Router that allows you to define your routes in a more declarative way.
Generating a new React project
The framework mode is the one that offers the most features (check comparison table), hence it has a few dependencies than the other modes. For that reason, setting it up manually it’s a bit tricky. A better idea is to generate a new project using the following command:
npx create-react-router@latest my-react-router-app
Where my-react-router-app
is the name of our project. This command will generate a new React project with the all the necessary dependencies to use React Router in framework mode.
NOTE
Later in this guide, and as an exercise, we’ll see how to add the framework mode to an already existing React project.
Once we have our project created, we can navigate to the root folder and start the development server:
cd my-react-router-appnpm run dev
Important Files
If we open the project in our code editor, we should see a bunch of files and folders, but the most important ones are inside the app
folder:
app/root.tsx
: This is the entry point of our application, from where we export:- The
Route.LinksFunction
. - The
Layout
component. - The
App
component. - An
ErrorBoundary
component.
- The
app/routes.ts
: This is where we define our routes.
Defining Routes
Routes must be defined in the app/routes.ts
file. For example, let’s define a simple route that renders a route module named home.tsx
when the URL is /
:
import { type RouteConfig, index } from '@react-router/dev/routes'
export default [ route('/', 'routes/home.tsx')] satisfies RouteConfig
The route
function is known as a route matcher; this route matcher takes two arguments:
- A URL pattern to match the URL, in this case ’/’.
- A file path to the route module that will be rendered when the URL matches the pattern.
Since having a route for the home page is a common use case, React Router provides a helper function called index
that does the same as route('/', 'routes/home.tsx')
:
import { type RouteConfig, index } from '@react-router/dev/routes'
export default [ route('/', 'routes/home.tsx') index('routes/home.tsx')] satisfies RouteConfig
These are the contents of routes/home.tsx
:
export default function Index() { return <h1>You are at Index</h1>}
IMPORTANT
Note that routes/home.tsx
is a route module and not just a React component. We have to export the React component as default
, otherwise we’ll get error:
You made a GET request to / but did not provide a `loader` for route "routes/home", so there is no way to handle the request.
Yeah, the error is a bit misleading, because we’re not loading any data in our componet; again, it happens because we didn’t export the component as default
.
Adding a Loader
Let’s define data loading for our route module. There are two ways to do this:
- Client Data Loading, using the
clientLoader
function. - Server Data Loading, using the
loader
function.
Client Data Loading
Let’s add a clientLoader
function to fetch a list of TODOs from the JSONPlaceholder API. We’ll render the list of TODOs in the home.tsx
route module:
import type { Route } from '../+types/root'
interface Todo { userId: number id: number title: string completed: boolean} // Better move this type to a separate file (types/todo.ts) and import it here.
export async function clientLoader() { const response = await fetch('https://jsonplaceholder.typicode.com/todos') if (!response.ok) { throw new Error('Failed to fetch todos') }
return response.json()}
export default function Home({ loaderData }: Route.ComponentProps) { return ( <main> <h1>You are at Index</h1> <h2>Todos</h2> <ul> {(loaderData ?? []).map((todo: Todo) => ( <li key={todo.id} className="py-4"> <h3>{todo.title}</h3> <p>{todo.completed ? 'Completed' : 'Not Completed'}</p> </li> ))} </ul> </main> )}
Yeah, I know, it’s a lot of code for rendering just a TODO list!, but let’s break it down:
- We define a
Todo
type that represents the structure of the data we’re going to fetch. - Note that
clientLoader
runs on the client side, like a traditional SPA; open the network tab to verify the request. - In the React component, we destructure the
loaderData
prop to access the data fetched by theclientLoader
function. Then we render the list of TODOs.
TIP
When testing this code, the browser’s console will show a recommendation to use the HydrateFallback, so I added:
// HydrateFallback is rendered while the client loader is runningexport function HydrateFallback() { return <div>Loading...</div>;}
Nice! So no need of defining React loading states.
Server Data Loading
Server Data Loading works the same way as the clientLoader
, but it runs on the server side. Let’s add a loader
function to fetch the individual TODOs:
import type { Route } from '../+types/root'import { Link } from 'react-router'import type { Todo } from 'types/todo'
export async function loader({ params }: { params: { id: string } }) { const { id } = params const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) if (!response.ok) { throw new Error('Failed to fetch todos!') }
return await response.json()}
export default function Todo({ loaderData }: Route.ComponentProps) { const todo = loaderData ? (loaderData as Todo) : null
return ( <main> <Link to="/">Back Home</Link>
{todo && ( <div key={todo.id}> <h3>{todo.title}</h3>
<p>{todo.completed ? 'Completed' : 'Not Completed'}</p> </div> )} </main> )}
TIP
Feel free to wrap the TODOs in routes/home.tsx
in a Link
component to navigate to the individual TODO route with a click.
The loader
function runs on the server, so you won’t be able to see the network request in the browser’s console. We get good old HTML from the backend, which you can verify it by disabling JavaScript in the browser.
IMPORTANT
Remember to add a route in app/routes.ts
to render the todo.tsx
route module:
import { type RouteConfig, index, route } from '@react-router/dev/routes'
export default [ index('routes/home.tsx'), route('todo/:id', 'routes/todo.tsx')] satisfies RouteConfig
In the code above, we’re using a dynamic segment in the first argument of the route
function (todo:id
). We end up with a dynamic route matcher that matches the URL /todo/:id
and renders the todo.tsx
route module. The :id
part is an argument that we’ll be used in the loader
function to render the TODO with that id
.
Adding a Layout Route
It’s pretty common to have a layout component shared amongst several routes in our application. As you can see, both our route modules have a main element repeated (in real life scenarios we’d have way more than that). We can define a layout route in the app/root.tsx
file:
import { Layout } from '@react-router/dev/routes'
export default [ layout("./components/layout.tsx", [ index('routes/home.tsx'), route('todo/:id', 'routes/todo.tsx') ]),]
An this is the content of components/layout.tsx
:
import { Outlet } from 'react-router'
export default function Layout() { return ( <div> <header> <h1>TODO list app</h1> </header>
<main> <Outlet /> </main> </div> )}
So the way this works is that the Layout
component is rendered for all the nested routes, wrapping them, and the Outlet
component is used to render the child routes.
IMPORTANT
Every route in routes.ts
is nested inside the special root route, defined in the app/root.tsx
module (Remember the Layout
component we mentioned at the beginning?).
Nested routes
We can also define nested routes in our application. Let’s say we want to have a
Adding a 404 Route
This one is really simple, we just have to add a route matcher that matches any URL:
import { type RouteConfig, index, route } from '@react-router/dev/routes'
export default [ index('routes/home.tsx'), route('todo/:id', 'routes/todo.tsx'), route('*', 'routes/not-found.tsx')] satisfies RouteConfig
And the content of routes/not-found.tsx
:
export default function NotFound() { return <h1>404 - Not Found</h1>}